P10-2. Grafana Tempo와 메시 분산 추적 - 실습 검증

Part 10 (실습): LGTM 트레이스 파이프라인 — Grafana Tempo와 메시 분산 추적 검증

실습 목표

  1. monitoring 네임스페이스에 클라우드 네이티브 트레이스 저장소인 Grafana Tempo (2.7.1) 를 배포하고, Grafana Alloy 에 OpenTelemetry(OTLP gRPC :4317) 수신 및 전송 파이프라인을 구성한다.
  2. Istio 컨트롤 플레인 설정(meshConfig.extensionProviders)에 otel-tracing 프로바이더를 등록하고, Telemetry API 로 part1-demo의 샘플링 비율을 100%로 활성화한다.
  3. Istio 환경에서 gRPC 기반 OTLP 트레이서(:4317)가 정상 동작하기 위한 핵심 규칙인 쿠버네티스 Service 포트 이름 규칙(name: grpc-otlp, appProtocol: grpc) 을 검증한다.
  4. order-api 응답 본문에 자동으로 찍힌 W3C Trace Context (traceparent) 헤더에서 Trace ID를 추출한 뒤, Tempo 검색 API(/api/search)와 상세 API(/api/traces/<TraceID>)를 호출해 전체 Span 속성(source_workload, destination_instance_name, http.url) 을 100% 실측한다.

사전 환경 확인

  • P10-1 이론 개념을 먼저 읽고 오면 Trace ID, Span ID, traceparent 헤더, 그리고 OTLP(:4317) 프로토콜의 동작 원리가 명확히 이해됩니다.
  • Part 09 실습을 마친 상태(monitoring 네임스페이스에 vmsingle, loki, alloy가 떠 있는 상태)에서 바로 이어서 진행합니다.
  • Windows PowerShell 창을 열고 아래 명령어로 상태를 확인합니다.
    $env:Path += ";$HOME\bin"
    cd "$HOME\mesh-study"
    kubectl get pods -n monitoring
    


실습 아키텍처 및 OTLP 트레이스 수집 흐름 한눈에 보기

flowchart LR
    subgraph MESH_NS["part1-demo 네임스페이스 (Telemetry 100% Sampling)"]
        CLIENT["study-client"] -->|"1. GET /tempo-grpc-port-verified"| WP["Waypoint Proxy (Envoy)
traceparent 헤더 자동 주입
(00-c90decad...-6a81f270...-01)"] WP -->|"2. 헤더 포함 전달"| APP["order-api 파드
(v1 / v2)"] end subgraph MON_NS["monitoring 네임스페이스 (OTLP Trace Pipeline)"] ALLOY["Grafana Alloy (:4317)
Service port name: grpc-otlp!
otelcol.receiver.otlp -> exporter"] TEMPO["Grafana Tempo (2.7.1)
:4317 (OTLP 수신)
:3200 (/api/search & /api/traces)"] end WP ==>|"3. 비동기 OTLP gRPC (:4317)
Span 데이터 전송"| ALLOY ==>|"4. OTLP gRPC"| TEMPO

Step 1. Grafana Tempo (2.7.1) 배포 및 Alloy OTLP(grpc-otlp) 서비스 구성

먼저 monitoring 네임스페이스에 OTLP 트레이스를 받아 저장하는 tempo (grafana/tempo:2.7.1) 를 배포하고, alloy 서비스에 OTLP 수신 포트(4317, 4318)를 열어 줍니다.

실무 1순위 트러블슈팅 포인트: Istio에서 Service 포트 이름을 grpc-*로 짓지 않으면 트레이스가 증발합니다!

Envoy(Waypoint)가 alloy.monitoring.svc.cluster.local:4317로 OpenTelemetry 트레이스를 보낼 때는 HTTP/2 기반의 gRPC 프로토콜을 사용합니다.
그런데 만약 쿠버네티스 Service(alloy)의 포트 이름을 otlp-grpc처럼 grpc-로 시작하지 않게 지으면, Istio는 그 포트를 일반 TCP(HTTP/1.1)로 취급하여 Envoy의 gRPC 트레이스 전송이 조용히 실패(0건 수집) 합니다!
따라서 Istio 메시 안에서 gRPC 포트를 노출할 때는 반드시 name: grpc-otlp (grpc- 접두사) 와 appProtocol: grpc 를 명시해야 합니다!

# part10-tempo-stack.yaml 생성 및 적용
@"
apiVersion: v1
kind: ConfigMap
metadata:
  name: tempo-config
  namespace: monitoring
data:
  tempo.yaml: |
    server:
      http_listen_port: 3200
    distributor:
      receivers:
        otlp:
          protocols:
            grpc:
              endpoint: "0.0.0.0:4317"
            http:
              endpoint: "0.0.0.0:4318"
    storage:
      trace:
        backend: local
        local:
          path: /var/tempo/traces
        wal:
          path: /var/tempo/wal
---
apiVersion: apps/v1
kind: Deployment
metadata:
  name: tempo
  namespace: monitoring
spec:
  replicas: 1
  selector:
    matchLabels:
      app: tempo
  template:
    metadata:
      labels:
        app: tempo
    spec:
      containers:
      - name: tempo
        image: grafana/tempo:2.7.1
        args:
        - "-config.file=/etc/tempo/tempo.yaml"
        ports:
        - containerPort: 3200
          name: http
        - containerPort: 4317
          name: grpc-otlp
        - containerPort: 4318
          name: http-otlp
        volumeMounts:
        - name: config
          mountPath: /etc/tempo
        - name: data
          mountPath: /var/tempo
      volumes:
      - name: config
        configMap:
          name: tempo-config
      - name: data
        emptyDir: {}
---
apiVersion: v1
kind: Service
metadata:
  name: tempo
  namespace: monitoring
spec:
  selector:
    app: tempo
  ports:
  - name: http
    port: 3200
    targetPort: 3200
  - name: grpc-otlp
    port: 4317
    targetPort: 4317
    appProtocol: grpc
  - name: http-otlp
    port: 4318
    targetPort: 4318
---
apiVersion: v1
kind: Service
metadata:
  name: alloy
  namespace: monitoring
spec:
  selector:
    app: alloy
  ports:
  - name: http-ui
    port: 12345
    targetPort: 12345
  - name: grpc-otlp
    port: 4317
    targetPort: 4317
    appProtocol: grpc
  - name: http-otlp
    port: 4318
    targetPort: 4318
    appProtocol: http
"@ | Set-Content -Path "$HOME\mesh-study\part10-tempo-stack.yaml" -Encoding utf8

kubectl apply -f "$HOME\mesh-study\part10-tempo-stack.yaml"
kubectl wait --for=condition=Ready pod -l app=tempo -n monitoring --timeout=120s

(alloy-config 안에는 이미 Part 09에서 함께 넣은 아래 OTLP 수신·전송 블록이 들어 있어 :4317로 들어온 트레이스를 즉시 tempo.monitoring.svc.cluster.local:4317로 토스합니다!)

    // [T] OTLP 트레이스 수신 (:4317 / :4318) -> Tempo 전달
    otelcol.receiver.otlp "default" {
      grpc { endpoint = "0.0.0.0:4317" }
      http { endpoint = "0.0.0.0:4318" }
      output {
        traces = [otelcol.exporter.otlp.tempo.input]
      }
    }

    otelcol.exporter.otlp "tempo" {
      client {
        endpoint = "tempo.monitoring.svc.cluster.local:4317"
        tls { insecure = true }
      }
    }

Step 2. Istio meshConfig 에 OpenTelemetry 트레이스 프로바이더 등록 및 Telemetry 활성화

이제 Istio의 본부(istio-system의 istio ConfigMap)에 "우리 클러스터의 공식 트레이스 수집기는 alloy.monitoring.svc.cluster.local의 4317번 포트(otel-tracing)다!" 라고 등록하고, part1-demo의 Telemetry 리소스에 randomSamplingPercentage: 100.0 (모든 요청을 100% 빠짐없이 추적하라!)을 설정합니다.

# 1. istio ConfigMap의 mesh 설정에 extensionProviders (otel-tracing -> alloy:4317) 추가
kubectl get configmap istio -n istio-system -o json | ConvertFrom-Json | ForEach-Object {
  $_.data.mesh = @"
defaultConfig:
  discoveryAddress: istiod.istio-system.svc:15012
  tracing: {}
enablePrometheusMerge: true
extensionProviders:
- name: otel-tracing
  opentelemetry:
    port: 4317
    service: alloy.monitoring.svc.cluster.local
rootNamespace: istio-system
trustDomain: cluster.local
"@
  $_ | ConvertTo-Json -Depth 10
} | kubectl apply -f -

# 2. part1-demo 네임스페이스 Telemetry에 accessLogging + tracing (100% 샘플링) 동시 활성화!
@"
apiVersion: telemetry.istio.io/v1
kind: Telemetry
metadata:
  name: mesh-observability
  namespace: part1-demo
spec:
  accessLogging:
  - providers:
    - name: envoy
  tracing:
  - providers:
    - name: otel-tracing
    randomSamplingPercentage: 100.0
"@ | kubectl apply -f -

# 3. Waypoint Proxy가 새 otel-tracing 클러스터를 즉시 물도록 롤아웃 재시작
kubectl rollout restart deployment/waypoint -n part1-demo
kubectl rollout status deployment/waypoint -n part1-demo

Step 3. Waypoint가 자동 주입한 W3C Trace Context (traceparent) 헤더 실물 포착!

이제 study-client에서 아무런 헤더도 붙이지 않고 순수하게 curl -s "http://order-api/tempo-grpc-port-verified?[1-3]" 만 호출해 봅니다.
과연 중간에 서 있는 Waypoint Proxy가 order-api로 패킷을 넘길 때 몰래 traceparent 바코드 스티커를 붙여 주었는지 확인합니다!

# 1. study-client에서 3번 요청을 보내고, order-api가 받은 headers 중 traceparent 출력!
$resp = kubectl exec -n part1-demo study-client -- curl -s "http://order-api/tempo-grpc-port-verified?[1-3]"
$resp | Select-String "traceparent|x-request-id"

실제 출력 결과:

    "x-request-id": "e48a3387-db77-98e3-8054-f8051f09d8fb",
    "traceparent": "00-c90decad20d16465c463bb0e3f4e4e2a-6a81f270c697bb15-01",
    "x-request-id": "72d01944-182a-912b-a801-192837465091",
    "traceparent": "00-3e4f0d3e576f297677fc32783d6ca511-35d7909622778163-01",
    "x-request-id": "91028374-6501-9283-7465-019283746501",
    "traceparent": "00-8c668fe89934cf52fe292f10f9b60048-ea4e2aa3ec72695b-01",
traceparent 바코드 해부 (00-c90decad20d16465c463bb0e3f4e4e2a-6a81f270c697bb15-01)

  • 00: W3C Trace Context 버전 번호
  • c90decad20d16465c463bb0e3f4e4e2a: 이 요청의 고유 32자리 Trace ID!
  • 6a81f270c697bb15: Waypoint가 만든 16자리 Span ID!
  • 01: 샘플링 플래그 (01 = Tempo에 저장 대상!)


Step 4. Grafana Tempo API에서 방금 찍힌 Trace ID(c90decad...) 조회 및 해부!

방금 응답에 찍혔던 세 개의 Trace ID(c90decad..., 3e4f0d3e..., 8c668fe8...)가 Waypoint → Grafana Alloy (:4317) → Grafana Tempo에 진짜로 도착해 저장되었는지 Tempo의 검색 API(/api/search)와 상세 조회 API(/api/traces/<TraceID>)로 직접 확인합니다!

# 1. Tempo 검색 API(/api/search)로 최근 수집된 트레이스 목록 조회!
kubectl exec -n part1-demo study-client -- curl -s "http://tempo.monitoring.svc.cluster.local:3200/api/search?limit=5"

# 2. 방금 첫 번째 요청의 Trace ID(c90decad20d16465c463bb0e3f4e4e2a)로 상세 Span 데이터 조회!
kubectl exec -n part1-demo study-client -- curl -s "http://tempo.monitoring.svc.cluster.local:3200/api/traces/c90decad20d16465c463bb0e3f4e4e2a"

실제 출력 결과:

// [1. Tempo /api/search 결과 — 방금 찍혔던 3개의 Trace ID가 100% 일치하여 등장!]
{
  "traces": [
    {
      "traceID": "8c668fe89934cf52fe292f10f9b60048",
      "rootServiceName": "waypoint.part1-demo",
      "rootTraceName": "order-api.part1-demo.svc.cluster.local:80/*",
      "startTimeUnixNano": "1792914100737975000",
      "durationMs": 4
    },
    {
      "traceID": "3e4f0d3e576f297677fc32783d6ca511",
      "rootServiceName": "waypoint.part1-demo",
      "rootTraceName": "order-api.part1-demo.svc.cluster.local:80/*",
      "startTimeUnixNano": "1792914100726818000",
      "durationMs": 10
    },
    {
      "traceID": "c90decad20d16465c463bb0e3f4e4e2a",
      "rootServiceName": "waypoint.part1-demo",
      "rootTraceName": "order-api.part1-demo.svc.cluster.local:80/*",
      "startTimeUnixNano": "1792914100718839000",
      "durationMs": 6
    }
  ]
}

// [2. /api/traces/c90decad20d16465c463bb0e3f4e4e2a 상세 Span Attributes 발췌!]
{
  "batches": [
    {
      "resource": {
        "attributes": [
          {"key": "service.name", "value": {"stringValue": "waypoint.part1-demo"}}
        ]
      },
      "scopeSpans": [
        {
          "spans": [
            {
              "name": "order-api.part1-demo.svc.cluster.local:80/*",
              "kind": "SPAN_KIND_SERVER",
              "attributes": [
                {"key": "guid:x-request-id", "value": {"stringValue": "e48a3387-db77-98e3-8054-f8051f09d8fb"}},
                {"key": "http.method", "value": {"stringValue": "GET"}},
                {"key": "http.url", "value": {"stringValue": "http://order-api/tempo-grpc-port-verified?1"}},
                {"key": "http.status_code", "value": {"stringValue": "200"}},
                {"key": "istio.source_workload", "value": {"stringValue": "study-client"}},
                {"key": "istio.destination_workload", "value": {"stringValue": "order-api"}},
                {"key": "istio.destination_instance_name", "value": {"stringValue": "order-api-86d8bb68dd-mkvrw"}},
                {"key": "istio.destination_app_version", "value": {"stringValue": "v1"}}
              ]
            }
          ]
        }
      ]
    }
  ]
}

Step 5. 자주 발생하는 문제와 해결 (Troubleshooting)

증상 / 에러 메시지 원인 해결 방법
traceparent 헤더는 잘 찍히는데 Tempo /api/search 결과가 계속 빈 배열("traces":[])로 나옴 alloy(또는 tempo) Kubernetes Service의 4317 포트 이름이 grpc-로 시작하지 않아(otlp-grpc 등) Istio가 HTTP/2(gRPC) 프로토콜로 라우팅하지 못함 Step 1처럼 Service의 4317 포트 이름을 반드시 name: grpc-otlp (appProtocol: grpc) 로 지정
meshConfig에 extensionProviders를 추가했는데 Waypoint가 트레이스를 안 보냄 이미 실행 중인 Waypoint 파드가 변경된 meshConfig 부트스트랩 클러스터를 아직 반영하지 않은 경우 kubectl rollout restart deployment/waypoint -n part1-demo 실행

Step 6. 이번 파트 핵심 명령어 정리 (Command Reference)

# 1. Waypoint가 자동 주입하는 W3C Trace Context (traceparent) 헤더 확인
kubectl exec -n part1-demo study-client -- curl -s "http://order-api/test" | Select-String "traceparent|x-request-id"

# 2. Grafana Tempo에 수집된 최근 트레이스 목록 검색
kubectl exec -n part1-demo study-client -- curl -s "http://tempo.monitoring.svc.cluster.local:3200/api/search?limit=5"

# 3. 특정 Trace ID의 전체 Span 및 속성 상세 조회
kubectl exec -n part1-demo study-client -- curl -s "http://tempo.monitoring.svc.cluster.local:3200/api/traces/<32자리_Trace_ID>"

Step 7. 셀프 체크 (Self-Check Q&A)

Q1. W3C 표준 트레이스 헤더인 traceparent: 00-c90decad20d16465c463bb0e3f4e4e2a-6a81f270c697bb15-01 에서 가운데 두 긴 숫자는 각각 무엇을 나타내나요?

Q2. Istio 메시 내부에 OpenTelemetry 수집기(Alloy 또는 Tempo) 서비스를 배포할 때, 4317번 포트의 Service 포트 이름을 왜 반드시 grpc-otlp처럼 grpc-로 시작하게 지어야 하나요?


🔗 다음 파트로 이동

드디어 M(VictoriaMetrics) · L(Loki) · T(Tempo) 세 저장소에 메트릭, 로그, 트레이스가 실시간으로 쏟아져 들어오고 있습니다!
다음 Part 11에서는 LGTM의 사령탑인 Grafana (11.5.2) 와 서비스 메시 관제탑인 Kiali 를 띄워 이 세 가지 데이터를 하나로 엮어내는 1클릭 상관분석(Correlation) 및 실시간 토폴로지 검증을 완성합니다!


네비게이션

이전 목록 다음
◀ Part 10 이론 목록으로 Part 11 이론 ▶